# error codes:
# 1 - timeout 
# 2 -  tcp errors
# 3 - dns error. can't resolve name and etc. (see EV::ADNS)
# 4- can't create socket
# 5 - reserved fo future use
# 6 - something wrong with socks server ( peer probably is not socks5 or it unexpectedly closed connection)
# 7 - authentification error (method, login/pass)
# 8 - socks5 can't connect to specified host
# 9 - address type is not implemented (probably ip6)
package Net::Coro::EV::Connect::Socks5;
use common::sense;
our $VERSION = 0.01;

use EV;
use Net::Coro::EV::Connect::Socket;

our $TRACE = 0;
our $TIMEOUT = 20;
use constant {
	SCK_NONE => 0,
	SCK_HELLO_1 => 1,
	SCK_HELLO_ACC => 2,
	SCK_HELLO_2 => 3,
	SCK_LISTEN => 4,
	
	TP_NONE => 0,
	TP_CONNECT => 0x01,
	TP_BIND => 0x02, # reserved
	TP_UDP => 0x03, # reserved
};

sub new {
	my ($class, $sckhost, $sckport, $socket) =@_;
	
	my $this ={};
	$this->{_STATE} = SCK_NONE;
	$this->{_TYPE} = TP_NONE;
	
	$socket = new Net::Coro::EV::Connect::Socket if !$socket;
	$this->{_SOCKET} = $socket;

	$this->{_SCKHOST} = $sckhost; # socks host
	$this->{_SCKPORT} = $sckport; # socks port
	$this->{_TIMEOUT} = $TIMEOUT;
	
	bless $this, $class;
	return $this;
}
sub type {'socks5'}
sub child {
	my ($this) = @_;
	my $child = {};
	bless $child, ref $this;
		
	while (my ($key, $val) = each(%$this)) {
		$child->{$key} = $val;
	}
	$child->{_SOCKET} = $this->{_SOCKET}->child;
	delete $child->{_CALLER};
	return $child;
}

sub auth {
	my ($this, $name, $pass) = @_;
	$this->{_NAME} = $name; # socks auth name
	$this->{_PASS} = $pass; # sock auth pass
}

sub address {
	my ($this, $sckhost, $sckport) = @_;
	if (defined $sckhost) {
		$this->{_SCKHOST} = $sckhost; # socks host
		$this->{_SCKPORT} = $sckport; # socks port
	}
	return ($this->{_SCKHOST}, $this->{_SCKPORT});
}


sub connect {
	my ($this, $host, $port, $caller) = @_;
	die("error: socks5 connect to $host:$port. current: state=".$this->{_STATE}.", type=".$this->{_TYPE}) if defined $this->{_WIO};
	utf8::downgrade($host);
	utf8::downgrade($port);
	
	$this->{_CALLER} = $caller;
	$this->{_HOST} = $host;
	$this->{_PORT} = $port;
	$this->{_STATE} = SCK_HELLO_1;
	$this->{_TYPE} = TP_CONNECT;

	print "TRACE $this->{_ID}, Socks5 connection begining - $this->{_SCKHOST}:$this->{_SCKPORT}\n" if $TRACE;
	$this->{_SOCKET}->connect($this->{_SCKHOST}, $this->{_SCKPORT}, $this);
}

sub onconnect {
	my ($this, $wio, $wtm, $serv_ip, $serv_port) = @_;
	return $this->err($this->{_SOCKET}->errm, $this->{_SOCKET}->errc) if !$wio;
	
	$this->{_WIO} = $wio;
	$this->{_WTM} = $wtm;
	
	$this->{_WIO}->data($this);
	$this->{_WIO}->events (EV::WRITE);
}


sub onwrite {
	my ($this) = @_;
	
	my $request = '';
	if ($this->{_STATE} == SCK_HELLO_1) {
		my $code = (defined $this->{_NAME})?0x02:0x00;
		$request = pack("C3", 0x05, 0x01, $code);
	}
	
	if ($this->{_STATE} == SCK_HELLO_ACC) {
		$request = pack("C2", 0x01, length($this->{_NAME})).$this->{_NAME}.
			pack("C1", length($this->{_PASS})).$this->{_PASS};
	}
	
	if ($this->{_STATE} == SCK_HELLO_2) {
		$request = pack("C5", 0x05, TP_CONNECT, 0x00, 0x03, length($this->{_HOST})).
			$this->{_HOST}.pack("n",$this->{_PORT});
	}
	
	return $request;
}


sub onread {
	my ($this, $response) = @_;	# plain text
	
	if ($this->{_STATE} == SCK_HELLO_1) {
		# check first socks response
		my ($sck_version, $sck_method) = unpack("C*", $response);
		if (!$sck_version or $sck_version ne '5') {
			$this->err("socks5[hello stage]: not socks5; resp: $sck_version, $sck_method",6);
			return;
		}
		
		if ($sck_method eq '0') {
			$this->{_STATE} = SCK_HELLO_2;
		} elsif ($sck_method eq '2') {
			$this->{_STATE} = SCK_HELLO_ACC;
		} else {
			$this->err("socks5[hello stage]: socks doesn't support authenification methods; but suport: $sck_method",7);
			return;
		}
		
		# if all good
		$this->{_WIO}->events (EV::WRITE);
		return;
	}
	
	
	if ($this->{_STATE} == SCK_HELLO_ACC) {
		# check account socks response
		my ($sck_version, $status) = unpack("C*", $response);
		if (!$sck_version or $sck_version ne '1') {
			$this->err("socks5[auth stage]: not socks5, resp: $sck_version, $status",6);
			return;
		}
		
		if ($status ne '0') {
			$this->err("socks5[auth stage]: probably login/pass incorrect. error code : $status",7) ;
			return;
		}
		
		# if all good
		$this->{_STATE} = SCK_HELLO_2;
		$this->{_WIO}->events (EV::WRITE);
		return;
	}
	
	if ($this->{_STATE} == SCK_HELLO_2) {
		# check second socks response
		my $ret_addr = bytes::substr($response, 4); # cat return adress
		my ($sck_version, $sck_code, $reserved, $ip_type, $tmp_rest) = unpack("C4a*", $response);
		if (!$sck_version or $sck_version ne '5') {
			$this->err("socks5[confirm stage]: not socks5, resp: $sck_version $sck_code $reserved $ip_type $tmp_rest",6);
			return;
		}

		if ($sck_code ne '0') {
			$this->err("socks5[confirm stage]: socks can't connect to(bind on) host [$sck_code]; resp: ".
				"$sck_version $sck_code $reserved $ip_type $tmp_rest", 8);
			return;
		}
		
		my ($ret_host,$ret_port) = $this->ret_address($ip_type, $ret_addr);
		if (!$ret_host) {
			$this->err("socks5[confirm stage]: anknown adress type: $ip_type", 9);
			return;
		}
		
		if ($this->{_TYPE} == TP_CONNECT) {
			$this->callback(delete($this->{_WIO}),delete($this->{_WTM}), $ret_host, $ret_port);
			return;
		}
		if ($this->{_TYPE} == TP_BIND) {
			(delete $this->{_CBBIND})->($ret_host, $ret_port);
			# continue READ from socket 
			$this->{_STATE} = SCK_LISTEN;
			return;
		}
	}
	
	if ($this->{_STATE} == SCK_LISTEN) {
		my $ret_addr = bytes::substr($response, 4); # cat return adress
		my ($sck_version, $sck_code, $reserved, $ip_type, @tmp_rest) = unpack("C*", $response);
		if ($sck_code ne '0') {
			$this->err("socks5[listen]: error occure while socks listen [$sck_code]; resp: ".
				"$sck_version $sck_code $reserved $ip_type ". join (' ',@tmp_rest), 8);
			return;
		}
		
		my ($ret_host,$ret_port) = $this->ret_address($ip_type, $ret_addr);
		if (!$ret_host) {
			$this->err("socks5[listen]: anknown adress type: $ip_type", 9);
			return;
		}
		
		$this->callback(delete($this->{_WIO}),delete($this->{_WTM}), $ret_host, $ret_port);
		return;
	}
}


sub ret_address {
	my ($this, $ip_type, $ret_addr) = @_;
	
	my ($ret_host,$ret_port);
	if ($ip_type == 0x01) {
		my ($ip1, $ip2, $ip3, $ip4);
		($ip1, $ip2, $ip3, $ip4, $ret_port) = unpack("C4n", $ret_addr);
		$ret_host = "$ip1.$ip2.$ip3.$ip4";
	} 
	elsif ($ip_type == 0x03) {
		my $host_len = unpack("C", $ret_addr);
		$ret_host = bytes::substr($ret_addr, 1, $host_len, '');
		($host_len, $ret_port) = unpack("Cn", $ret_addr);
	}
	else {
		return;
	}
	
	return ($ret_host,$ret_port);
}


######## COMMON ARCHITECTURAL METHODS AND EVENT HANDLERS ###################
sub onwritten {
	my ($this, $w) = @_;
	$w->events (EV::READ);
}

sub onclose {
	my ($this) = @_;
	# make desigion what to do
	# default is to send error
	$this->err("socks5: connectnion closed by peer",6);
}

sub onbreak {
	my ($this, $msg, $code) = @_;
	# make desigion what to do
	# default is to send error
	$this->err($msg, 2);
}

sub callback {
	my ($this, @resp) = @_;
	undef $this->{_ERRM};
	undef $this->{_ERRC};
	print "TRACE $this->{_ID}, Socks5 connection done \n" if $TRACE;
	(delete $this->{_CALLER})->onconnect(@resp);
}

sub err {
	my ($this, $msg, $code) = @_;
	$this->{_ERRM} = $msg;
	$this->{_ERRC} = $code;

	print "TRACE $this->{_ID}, Socks5 connection error - $msg (code $code)\n" if $TRACE;
	$this->close;
	(delete $this->{_CALLER})->onconnect();
}


sub close {
	my ($this) =@_;
	$this->{_STATE} = SCK_NONE;
	$this->{_TYPE} = TP_NONE;
	
	my $tm = delete $this->{_WTM};
	$tm->stop() if defined $tm;
	
	my $w = delete $this->{_WIO};
	if (defined $w) {
		$w->stop();
		my $fh = $w->fh;
		shutdown($fh, 2);
		CORE::close($fh);
	}
}

sub stop {
	my ($this) = @_;
	$this->{_SOCKET}->stop();
	$this->close();
	delete $this->{_CALLER};
}



############ COMMON PROPIRTIES ##############
sub errc {$_[0]->{_ERRC}}
sub errm {$_[0]->{_ERRM}}

sub timeout {
	my ($this, $val) = @_;	
	if (defined $val) {
		$this->{_TIMEOUT} = $val;
		$this->{_SOCKET}->timeout($val);
	}
	return $this->{_TIMEOUT};
}

sub bindip {$_[0]->{_SOCKET}->bindip($_[1])}

sub id {
	my ($this, $id) = @_;
	if (defined $id) {
		$this->{_ID} = $id;
		$this->{_SOCKET}->id($id);
	}
	$this->{_ID};
}
1;